<?php
/**
 * @desc Шаблонизатор.
 */
class CFoxtpl2
{
    /**
     * @var array Массив переменных, которые будут подставляться в шаблон
     */
    public $vars = array();
    static $debug = false; // Принудительная компиляция шаблонов

    public $basepath;
    public $common_path;
    public $template_path;

    protected $compiledir;

    /*************************************************/
    public $compile_time;
    protected $ob_started = false;
    protected $is_publish = false;
    protected $body = "";
    protected $searches = array();
    protected $replaces = array();

    function __construct()
    {
        $this->basepath = $_SERVER['DOCUMENT_ROOT'] . "/templates/";
        $this->compiledir = "_compiled/";
        //$this->vars["const"] = CSetup::getConst();

    }

    function setBasepath($path){
        $this->basepath = $path;
    }

    /**
     * @desc Обрабатывает шаблон и возвращает результат в переменную
     * @param string $filename Относительный путь к шаблону
     * @returns string Строка содержащая результат обработки шаблона
     */
    public function fetch($filename)
    {
        ob_start();
        $this->parse( $filename );
        $this->ob_started = false;
        return ob_get_clean();
    }

    /**
     * @desc Обрабатывает шаблон и выводит результат на экран
     * @param string $filename Относительный путь к шаблону
     */
    public function parse($filename)
    {
        if(empty($filename)) throw new Exception("Имя шаблона не может быть пустой строкой",666);
        $this->is_publish = false;
        $this->searches[] = "@{publish}(.+?){/publish}@is";
        $this->replaces[] = "";
        $this->searches[] = "@{([/]?)nopublish}@i";
        $this->replaces[] = "";
        $compiled_name = $this->compile( $filename );
    }

    /**
     * @desc Обрабатывает шаблон и сохраняет результат в файл. При обработке использует специфический для публикации алгоритм.
     * @param string $filename    Относительный путь к шаблону
     * @param string $destination имя файла куда сохранить результат
     */
    public function publish( $filename, $destination )
    {
        $this->is_publish = $filename;
        $this->searches[] = "@{publish}(.+?){/publish}@is";
        $this->replaces[] = "";
        $this->searches[] = "@{([/]?)nopublish}@i";
        $this->replaces[] = "";
        $compiled_name = $this->compile( $filename );
        file_put_contents( $destination, ob_get_clean() );
        $this->ob_started = false;
    }

    public function templateExists($filename){
        return  file_exists($this->basepath . $filename);
    }

    public function setTheme($theme){
        $this->basepath = $_SERVER['DOCUMENT_ROOT']."/view/themes/".$theme."/";
    }


    /**
     * @desc Проверяем создаем, компилируем файл
     * @param string $filname Путь к файлу
     * @return string Имя скомпилированного шаблона
     */
    private function compile( $filename )
    {
        // проверяем на валидность имени шаблона
        if(empty($filename)) throw new Exception("Имя шаблона не может быть пустой строкой",666);
        if( strpos( $filename, ".." ) ) throw new Exception("Ошибка подключения шаблона '$filename'. Используются запрещенные смволы [..]",666);
        $filename = trim( $filename );

        // проверяем на существующий файл
        if ( file_exists($this->basepath . $filename) ){
            $this->template_path = $this->basepath;
        }
        else{
            throw new Exception("Шаблон ".$this->basepath."/".$filename." не найден",666);
        }

        $compiled_name = $this->basepath . $this->compiledir . str_replace("/","_",$filename). ($this->is_publish ? "_pub" :"" ). ".php";
        $this->compile_time = microtime(1);
        // если шаблон новый, или не существует, или мы выставили принудительное обновление
        if( $this->is_publish || self::$debug || !file_exists( $compiled_name ) || ( filemtime( $this->template_path.$filename ) > filemtime( $compiled_name ) ) )
        {
            $this->body = file_get_contents( $this->template_path . $filename );


            $this->process_specials();

            //Замена переменных $var.param.param2.... на $var["param"]["param2"][....
            //Очень важно делать ее до начала основной компиляции
            $compiled = preg_replace( $this->searches, $this->replaces, $this->body );
            /*
                        if( $this->is_publish )
                        {
                            $callback = array( $this, 'include_convert' );
                            $compiled = preg_replace_callback( "@{include file=\"(.+?)\"}@i", $callback, $compiled );
                        }
            */
            $process_tag_fn = array($this,'process_tag');

            //Вырезаем комментарии в формате {* ... *}
            $compiled = preg_replace("@{\*.*?\*}@s","",$compiled);

            $compiled = preg_replace_callback("@{.*?}@",$process_tag_fn,$compiled);

            if(!is_dir(dirname($compiled_name))) mkdir(dirname($compiled_name));
            file_put_contents( $compiled_name, $compiled );
        }
        // открыли потоки
        extract( $this->vars );
        if(!$this->ob_started) $this->ob_started = ob_start();
        // включили
        $current_errrep = error_reporting();
        error_reporting($current_errrep &~ E_NOTICE); //Скрываем Notice возникшие в шаблонах

        include $compiled_name;

        error_reporting($current_errrep);

        // запомнить выполненное время
        $this->compile_time = microtime(1) - $this->compile_time;
        // возвратить имя скомпилированного файла
        return $compiled_name;
    }

    private function include_convert( $matches )
    {
        if( $matches[1]{0} == '$' )
        {
            $include_file = $this->vars[substr($matches[1],1)];
        }
        else
            $include_file = $matches[1];
//        $result_name = $this->basepath . $this->compiledir . str_replace("/", "_", $include_file ).".pub";
//        $tpl = new cfoxtpl();
        $this->parse( $include_file );
//        return "<!--#include virtual=\"$result_name\" -->";
    }

    /**
     * @desc Конвертирует переменные вида $var.param.param2 в $var["param"]["param2"]
     */
    private function vars_replace($matches)
    {
        $result = preg_replace('@\.([a-z0-9_]+)@i','["$1"]', $matches );
        return preg_replace('@\->([a-z0-9_]+)@i','->$1', $result );
    }

    protected function process_tag($match)
    {
        $result = $match[0]; //По умолчанию считаем что тег не стоит трогать

        if (preg_match("@{([a-z_]+?) (.*?)}@i",$match[0],$tag)) //Сложный тег
        {
            //Наши все переметры с постоянными величинами (в кавычках)
            preg_match_all("@([a-z_]+?)=\"([^\"]+)\"@i",$tag[2],$params_const);
            //Нашли все параметры с переменными величинами ($var)
            preg_match_all("@([a-z_]+?)=(\\\$[^ ]+)@i",$tag[2],$params_var);
            //Поменяли переменные
            $params_var[2] = $this->vars_replace($params_var[2]);
            //И объединили массивы назад
            $keys = array_merge($params_var[1],$params_const[1]);
            $values = array_merge($params_var[2],$params_const[2]);
            $params = (!empty($keys)) ? array_combine($keys,$values) : array($this->vars_replace($tag[2]));
            //Вызываем метод привязанный к заданному тегу
            if (method_exists($this,"tag_" . $tag[1])) $result = $this->{"tag_" . $tag[1]}($params);
        }
        else if (preg_match("@{([a-z_]+?)}@i",$match[0],$tag)) //Простой тег
        {
            if (method_exists($this,"tag_" . $tag[1])) $result = $this->{"tag_" . $tag[1]}(null);
        }
        else if (preg_match("@{(\\\$.+?)}@i",$match[0],$var)) //echo
        {
            $result = '<?=' . $this->vars_replace($var[1]) . "?>";
        }
        return $result;
    }


    //Обработчики супертегов

    protected function tag_include($params)
    {
        if (empty($params['file'])) throw new Exception('Ошибка в синтаксисе тега {include} отсутствует параметр file', 666);
        $out = "";
        foreach ($params as $key=>$value)
        {
            if ($key=='file') continue;
            $out .= '<? $this->vars[\''.$key.'\'] = '.$value.';?>';
        }
        $out .="<?=\$this->parse(\"".$params['file']."\")?>";
        return $out;
    }

    protected function tag_if($params)
    {
        if (!empty($params['test']))
            return "<?if (".$params['test']."):?>";
        else
            return "<?if (".array_pop($params)."):?>";
    }

    protected function tag_active($params)
    {
        $active_class = 'active';
        if(isset($params['class'])){
            $active_class = $params['class'];
        }
        if(isset($params['uri'])){
            return "<?=preg_match('{$params['uri']}', \$_SERVER['REQUEST_URI'])?'active' : ''?>";
        }
        if(isset($params['uri_strong'])){
            return "<?= \$_SERVER['REQUEST_URI'] == '{$params['uri_strong']}' ? 'active' : '' ?>";
        }
        $parts = array();
        foreach($params as $key=>$val){
            $parts[] = "@\$_GET['$key'] == '$val'";
        }
        return "<?=(".join("&&", $parts).")?'{$active_class}':''?>";
    }

    protected function tag_ifcount($params)
    {
        if (empty($params['values'])) throw new Exception('Ошибка в синтаксисе тега {ifcount} отсутствует параметр values',666);
        if (empty($params['item'])) throw new Exception('Ошибка в синтаксисе тега {ifcount} отсутствует параметр item',666);
        $result = "";
        foreach( explode(",",$params['values']) as $experssion )
        {
            if( $result != "" ) $result .= " && ";
            $result .= "count(".$params['item'].")$experssion";
        }
        if( empty( $result ) )
            throw new Exception('Ошибка в синтаксисе тега {ifcount} не определены границы проверки',666);
        return "<?if ( $result ): ?>";;
    }

    protected function tag_elseif($params)
    {
        if (empty($params['test'])) throw new Exception('Ошибка в синтаксисе тега {elseif}',666);
        return "<?elseif (".$params['test']."):?>";
    }

    protected function tag_elseifcount($params)
    {
        if (empty($params['values'])) throw new Exception('Ошибка в синтаксисе тега {elseifcount} отсутствует параметр values',666);
        if (empty($params['item'])) throw new Exception('Ошибка в синтаксисе тега {elseifcount} отсутствует параметр item',666);
        $result = "";
        foreach( explode(",",$params['values']) as $experssion )
        {
            if( $result != "" ) $result .= " && ";
            $result .= "count(".$params['item'].")$expression";
        }
        if( empty( $result ) )
            throw new Exception('Ошибка в синтаксисе тега {elseifcount} не определены границы проверки',666);
        return "<?elseif ( $result ): ?>";;
    }

    protected function tag_set($params)
    {
        if (empty($params['item'])) throw new Exception('Ошибка в синтаксисе тега {set} отсутствует параметр item',666);
        if (!isset($params['values'])) throw new Exception('Ошибка в синтаксисе тега {set} отсутствует параметр values',666);
        return "<? ".$params['item']." = ".$params["values"].";?>";
    }
    protected function tag_foreach($params)
    {
        if(empty($params['from']) or empty($params['item']))
        {
            throw new Exception('Ошибка в синтаксисе тега {foreach}',666);
        }

        $var_init = "";
        $counter_init = "";
        $counter_inc = "";

        if(!empty($params['counter']))
        {
            $counter_init = $params['counter'] . "=0;";
            $counter_inc = $params['counter'] . "++;";
        }

        if(($i = strpos($params['item'], "=>")) === false) $i = 1; else $i += 3;
        $item = substr($params['item'], $i);
        $var_init = "\$this->vars[\"{$item}\"] = \${$item};";

        static $var_counter;
        $var_counter ++;

        $result =
            "<?
                $counter_init
                \$_tmp_arr_{$var_counter} = {$params['from']};
                if(is_array(\$_tmp_arr_{$var_counter}) or \$_tmp_arr_{$var_counter} instanceof ArrayAccess)
                    foreach (\$_tmp_arr_{$var_counter} as ".$params['item']."):
                    $counter_inc
            ?>";
        return $result;
    }

    protected function tag_verb($params)
    {
        list($one,$two,$five) = explode(",",$params['values']);
        if (empty($one) or empty($two) or empty($five)) throw new Exception('Недостаточно числительных в теге {verb}');
        return "<?=chumanist :: verb(".$params['item'].",'$one','$two','$five')?>";
    }

    protected function tag_echo($params)
    {
        if (!isset($params["default"])) $params["default"]="";
        return "<? if (!empty(".$params['item'].")) echo ".$params["item"]."; else echo '".$params["default"]."'; ?>";
    }


    protected function tag_verbcount($params)
    {
        list($one,$two,$five) = explode(",",$params['values']);
        if (empty($one) or empty($two) or empty($five)) throw new Exception('Недостаточно числительных в теге {verb}');
        return "<?=chumanist :: verb(count(".$params['item']."),'$one','$two','$five')?>";
    }

    protected function tag_timeformat($params)
    {
        $today = isset($params["today"]) ? $params["today"] : "";
        return "<?=chumanist :: time_format(".$params['item'].", \"{$today}\")?>";
    }

    protected function tag_datetime($params)
    {
        $format = isset($params["format"]) ? $params["format"] : "d.m.Y H:i";
        return "<? \$__time = is_numeric({$params['item']}) ? {$params['item']} : strtotime(".$params['item'].");?><?=str_replace('v', chumanist::month_rod(date('n',\$__time)), chumanist::date('".$format."',\$__time));?>";
    }

    protected function tag_teaser($params)
    {
        $html = isset($params["html"]) ? $params["html"] : "false";
        return "<?=chumanist :: teaser(str_replace('&nbsp;', ' ', ".$params['item']."), ".$params['size'].", ".$html.")?>";
    }

    protected function tag_count($params)
    {
        return "<?=count(".$params['item'].")?>";
    }

    protected function tag_array($params)
    {
        $array = "\"".implode("\",\"",explode(",",$params['values']))."\"";
        return "<?=((\$_var=array($array))&&isset(\$_var[".$params['item']."])?\$_var[".$params['item']."]:\"".$params['default']."\")?>";
    }

    protected function tag_urlencode($params)
    {
        return "<?=urlencode(".$params['item'].")?>";
    }

    protected function tag_escape($params)
    {
        if(isset($params['isset']))
        {
            $beg_isset = "<? if (isset(".$params['item'].")) :?>";
            $end_isset = "<? endif; ?>";
        } else {
            $beg_isset = '';
            $end_isset = '';
        }

        if (isset($params['type']) && $params['type'] == 'xml') {
            return $beg_isset."<?=XMLCleaner::fastClean(".$params['item'].")?>".$end_isset;
        }
        return $beg_isset."<?=htmlspecialchars(".$params['item'].", ENT_QUOTES)?>".$end_isset;
    }

    protected function tag_striptags($params)
    {
        if(isset($params['isset']))
            return "<?=str_replace(array(\"'\", \"&nbsp;\", \"\\n\", '<', '>'), array('&apos;', '', '', '&lt;', '&gt;'), strip_tags(".$params['item']."))?>";
        else
            return "<?=isset(".$params['item'].") ? str_replace(array(\"'\", \"&nbsp;\", \"\\n\"), array(\"&apos;\", '', ''), strip_tags(".$params['item'].")): ''?>";
    }

    protected function tag_json($params)
    {
        return "<?=json_encode(".$params['item'].")?>";
    }

    protected function tag_virtual($params)
    {
        if(CSetup::$ZONE == "local")
        {
            return "<?=CRemoteContent::getFile(\"http://\".\$_SERVER['HTTP_HOST'].\"".$params['file']."\", \"\");?>";
        }
        return "<!--#include virtual=\"".$params['file']."\" -->";
    }

    protected function tag_wrapper($params)
    {
        return "<?=preg_replace(\"/([^\s]{".$params['length']."})/us\",'$1&#8203;',".$params['item'].")?>";
    }

    protected function tag_options($params)
    {
        $param_value = isset($params['value']) ? $params['value'] : "__false__";
        $param_name = isset($params['name']) ? $params['name'] : "__false__";

        if(empty($params['from']))
        {
            throw new Exception('Ошибка в синтаксисе тега {options}', 667);
        }

        if(isset($params['defvalue']))
        {
            $default = '$__default = is_array('.$params['defvalue'].') ? '.$params['defvalue'].' : array( '.$params['defvalue'].' => true );';
            $defexpression = '<?=isset($__default[$__value])? " selected" : ""?>';
        }
        else
        {
            $default = "";
            $defexpression = "";
        }

        return '<? '.$default.'

foreach('.$params['from'].' as $__key => $__item):

    if(is_array($__item))
    {
        $__value = isset($__item["'.$param_value.'"]) ? $__item["'.$param_value.'"] : $__key;
        $__name  = "'.$param_name.'";
        foreach ($__item as $___key=>$___val)
        {
        	$__name  = str_replace($___key, $___val, $__name);
        }
    }
    else
    {
        $__value = $__key;
        $__name  = $__item;
    }
?><option value="<?=$__value?>"'.$defexpression.'><?=$__name?></option>
<? endforeach; ?>';
    }

    //Различные директивы которые удобнее парсить кучей, так как возможна прямая замена
    protected function process_specials()
    {
        $this->searches[] = "@{/foreach}@i";
        $this->replaces[] = "<?endforeach;?>";
        $this->searches[] = "@{else}@i";
        $this->replaces[] = "<?else:?>";
        $this->searches[] = "@{/if}@i";
        $this->replaces[] = "<?endif;?>";
    }

}
